//-----------------------------------------------------------------------
// <copyright file="Program.cs" company="Anthony Steele">
// This source forms part of the DuplicateFinder utility
// Hosted at http://www.codeplex.com/DuplicateFinder
//
// This source is available under the terms of the Microsoft Public License (Ms-PL)
// See http://duplicatefinder.codeplex.com/license
// All other rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DupFinder
{
    using System;
    using System.Globalization;
    using System.IO;

    using DupFinder.Logging;
    using DuplicateFinderLib;

    /// <summary>
    /// The command-line handler
    /// </summary>
    public class DuplicateFinderProgram
    {
        #region constants

        /// <summary>
        /// Text to display for user help docs
        /// </summary>
        private static readonly string HelpText =
            LibraryVersion.ProgramName + 
            " v" + LibraryVersion.Version.ToString("#0.0#", CultureInfo.CurrentCulture) + " " +
            LibraryVersion.Copyright + "\n" +
            "Usage: DupFinder [-r] [-i] [-tn] [-wn] [-ofile] [-ffile] filemask [filemask]\n" +
            " r: recurse\n" +
            " i: instrument times \n" +
            " t: set threshhold to a number >= 1\n" +
            " w: set first line width to a number >= 1\n" +
            " f: focus on file\n" +
            " e: Exclude file name\n" + 
            " x: Exclude lines that start with the prefix\n" +
            " o: Log output is in XML format, not plain text\n" +
            " filemask: a file name, or a wildcard, e.g. \"*.cs\"\n";

        #endregion

        #region data 

        /// <summary>
        /// Directory to process from
        /// </summary>
        private readonly string rootDirectory;

        /// <summary>
        /// The engine that does the duplicate finding
        /// </summary>
        private FileDuplicateFinder fileDupFinder;

        // data from the command-line options

        /// <summary>
        /// Recurse into subdirectories
        /// </summary>
        private bool recursive;

        /// <summary>
        /// Track how long it took
        /// </summary>
        private bool instrumentTimes;

        /// <summary>
        /// Has the start time been set
        /// </summary>
        private bool hasStartTime;

        /// <summary>
        /// File to focus on
        /// </summary>
        private string focusFile;

        /// <summary>
        /// Minimum number of lines before its declared a duplicate
        /// </summary>
        private int duplicateThreshold = -1;

        /// <summary>
        /// First line must be this long
        /// </summary>
        private int firstLineMinWidth = -1;

        /// <summary>
        /// This handles creating the output
        /// </summary>
        private ILogProvider logHandler;
        
        // working data

        /// <summary>
        /// Time that the program started
        /// </summary>
        private DateTime startTime;

        /// <summary>
        /// Time that data reading in finished
        /// </summary>
        private DateTime endReadTime;

        #endregion

        #region constructor

        /// <summary>
        /// Initializes a new instance of the DuplicateFinderProgram class
        /// </summary>
        public DuplicateFinderProgram()
        {
            this.rootDirectory = Directory.GetCurrentDirectory();
        }

        #endregion

        #region entry point

        /// <summary>
        /// Program entry point
        /// </summary>
        /// <param name="args">commandline params</param>
        public static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine(HelpText);
            }
            else
            {
                try
                {
                    DuplicateFinderProgram instance = new DuplicateFinderProgram();
                    instance.Process(args);
                }
                catch (ArgumentException ex)
                {
                    // an expected exception - show the message
                    Console.WriteLine(ex.Message);
                }
            }
        }

        #endregion

        #region worker procs

        /// <summary>
        /// read the commmandline args into the file duplicate finder, and run it
        /// </summary>
        /// <param name="args">the commandline arguments to process</param>
        public void Process(string[] args)
        {
            this.logHandler = new ConsoleLogProvider();

            this.fileDupFinder = new FileDuplicateFinder();
            this.fileDupFinder.OnDuplicate += this.OnDuplicateHandler;

            foreach (string argument in args)
            {
                this.ProcessArgument(argument);
            }

            this.logHandler.LogMessage(string.Format(CultureInfo.InvariantCulture, "Processing in {0}", this.rootDirectory));

            if (this.instrumentTimes)
            {
                this.ShowEndReadTime();
            }

            this.logHandler.LogFileCount(this.fileDupFinder.ReadCount);

            if (this.fileDupFinder.ReadCount < 1)
            {
                this.logHandler.Close();
                return;
            }

            // apply the argument options
            if (this.duplicateThreshold > 0)
            {
                this.fileDupFinder.DuplicateThreshold = this.duplicateThreshold;
            }

            if (this.firstLineMinWidth > 0)
            {
                this.fileDupFinder.FirstLineMinWidth = this.firstLineMinWidth;
            }

            if (!string.IsNullOrEmpty(this.focusFile))
            {
                int fileCount = this.fileDupFinder.FindDuplicates(this.focusFile);
                if (fileCount == 0)
                {
                    this.logHandler.LogMessage("No files matched " + this.focusFile);
                }
            }
            else
            {
                this.fileDupFinder.FindDuplicates();
            }

            this.logHandler.LogDuplicateCount(this.fileDupFinder.DuplicateCount);

            if (this.instrumentTimes)
            {
                this.ShowEndProcessTime();
            }

            this.logHandler.Close();
        }

        /// <summary>
        /// Read a number from a commandline option
        /// </summary>
        /// <param name="option">the option</param>
        /// <param name="optionType">the name of the option</param>
        /// <param name="minValue">check the minimum value</param>
        /// <returns>the value read from the option</returns>
        private static int ReadIntFromCommandlineOption(string option, string optionType, int minValue)
        {
            int result;

            string intValue = option.Substring(1);
            try
            {
                result = Convert.ToInt32(intValue, CultureInfo.CurrentCulture);
            }
            catch (FormatException ex)
            {
                string message = String.Format(
                    CultureInfo.CurrentCulture,
                    "{0} value \"{1}\" is not a number",
                    optionType,
                    intValue);
                throw new ArgumentException(message, ex);
            }

            if (result < minValue)
            {
                string message = String.Format(
                    CultureInfo.CurrentCulture,
                    "{0} value \"{1}\" must be more than {2}",
                    optionType,
                    intValue,
                    minValue);
                throw new ArgumentException(message);
            }

            return result;
        }

        /// <summary>
        /// Format a timespan for display
        /// </summary>
        /// <param name="ts">the timespan</param>
        /// <returns>text formatted for display</returns>
        private static string FormatTimespan(TimeSpan ts)
        {
            return String.Format(
                CultureInfo.CurrentCulture, 
                "{0:D2}:{1:D2}:{2:D2}", 
                ts.Hours, 
                ts.Minutes, 
                ts.Seconds);
        }

        /// <summary>
        /// A duplicate was found, display it
        /// </summary>
        /// <param name="sender">the engine reporting this duplicate</param>
        /// <param name="e">the duplicate data in event args</param>
        private void OnDuplicateHandler(object sender, DuplicateEventArgs e)
        {
                this.logHandler.LogDuplication(e);
        }

        /// <summary>
        /// Show the time that reading data ended
        /// </summary>
        private void ShowEndReadTime()
        {
            this.endReadTime = DateTime.Now;
            TimeSpan readDuration = this.endReadTime - this.startTime;
            string message = "Read duration " + FormatTimespan(readDuration);
            Console.WriteLine(message);
        }

        /// <summary>
        /// Show the time that processing data ended
        /// </summary>
        private void ShowEndProcessTime()
        {
            DateTime endTime = DateTime.Now;
            TimeSpan processDuration = endTime - this.endReadTime;
            string message = "Process duration " + FormatTimespan(processDuration);
            Console.WriteLine(message);
        }

        /// <summary>
        /// Process a commandline argument
        /// </summary>
        /// <param name="argument">the argument</param>
        private void ProcessArgument(string argument)
        {
            if (argument.StartsWith("-", StringComparison.Ordinal))
            {
                this.ProcessCommandLineOption(argument.Substring(1));
            }
            else
            {
                if (this.instrumentTimes && (!this.hasStartTime))
                {
                    this.hasStartTime = true;
                    this.startTime = DateTime.Now;
                }

                this.fileDupFinder.ReadFile(this.rootDirectory, argument, this.recursive);
            }
        }

        /// <summary>
        /// Process a commandline option
        /// </summary>
        /// <param name="option">the option</param>
        private void ProcessCommandLineOption(string option)
        {
            switch (option[0])
            {
                case 'r':
                    {
                        this.recursive = true;
                        break;
                    }

                case 't':
                    {
                        this.duplicateThreshold = ReadIntFromCommandlineOption(option, "Threshold", 2);
                        break;
                    }

                case 'w':
                    {
                        this.firstLineMinWidth = ReadIntFromCommandlineOption(option, "First line min width", 1);
                        break;
                    }

                case 'i':
                    {
                        this.instrumentTimes = true;
                        break;
                    }

                case 'f':
                    {
                        this.focusFile = option.Substring(1);
                        break;
                    }

                case 'e':
                    {
                        string excludeFileName = option.Substring(1);
                        excludeFileName = excludeFileName.Trim();
                        if (!string.IsNullOrEmpty(excludeFileName))
                        {
                            this.fileDupFinder.AddExcludedFileName(excludeFileName);
                        }

                        break;
                    }

                case 'o':
                    {
                        string logFileName = option.Substring(1);
                        logFileName = logFileName.Trim();

                        if (string.IsNullOrEmpty(logFileName))
                        {
                            throw new ArgumentException("No log file name given");
                        }

                        this.logHandler = new XmlLogProvider(logFileName);
                        break;
                    }

                case 'x':
                    {
                        string excludeLinePrefix = option.Substring(1);
                        excludeLinePrefix = excludeLinePrefix.Trim();
                        if (!string.IsNullOrEmpty(excludeLinePrefix))
                        {
                            this.fileDupFinder.AddIgnoredLinePrefix(excludeLinePrefix);
                        }

                        break;
                    }

                default:
                    {
                        string message = String.Format(
                            CultureInfo.CurrentCulture,
                            "Option \"{0}\" not recognised", 
                            option);
                        throw new ArgumentException(message);
                    }
            }
        }

        #endregion
    }
}
